package bancosys.tec.utils.reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.List;

import jmine.tec.utils.UtilsMessages;
import jmine.tec.utils.collection.Mapper;
import jmine.tec.utils.collection.MapperUtils;

/**
 * Classe com métodos utilitários para a API de reflection do java.
 * 
 * @author Adriano
 */
public final class ReflectionUtils {

    /**
     * Construtor privado.
     */
    private ReflectionUtils() {
        // vazio
    }

    /**
     * Procura a classe correspondente à este nome.
     * <p>
     * Procura primeiro no contextClassLoader e caso não encontre procura no callerClassLoader.
     * <p>
     * Se não encontrar em nenhum lança um ClassNotFoundException.
     * <p>
     * 
     * @param className O nome da classe
     * @return a classe
     * @throws ClassNotFoundException se a classe não for encontrada em nenhum lugar
     */
    public static Class<?> findClass(String className) throws ClassNotFoundException {
        return ReflectionUtils.findClass(className, ReflectionUtils.class.getClassLoader());
    }

    /**
     * Procura a classe correspondente à este nome.
     * <p>
     * Procura primeiro no contextClassLoader e caso não encontre procura no fallbackClassLoader.
     * <p>
     * Se não encontrar em nenhum lança um ClassNotFoundException.
     * <p>
     * 
     * @param className O nome da classe
     * @param fallbackClassLoader o classloader usado caso o context não tenha a definição da classe
     * @return a classe
     * @throws ClassNotFoundException se a classe não for encontrada em nenhum lugar
     */
    public static Class<?> findClass(String className, final ClassLoader fallbackClassLoader) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader != null) {
            try {
                return ReflectionUtils.forName(className, classLoader);
            } catch (ClassNotFoundException e) {
                // silent catch pra tentar de novo no fallback classloader
            }
        }

        return ReflectionUtils.forName(className, fallbackClassLoader);
    }

    /**
     * Procura neste class loader lançando ClassNotFoundException se não encontrar.
     * 
     * @param className o nome da classe
     * @param classLoader o class loader que deve ser utilizado
     * @return a classe
     * @throws ClassNotFoundException se não encontrar
     */
    private static Class<?> forName(String className, final ClassLoader classLoader) throws ClassNotFoundException {
        return Class.forName(className, false, classLoader);
    }

    /**
     * Cria um objeto a partir da classe informada.
     * 
     * @param <T> Classe do novo objeto.
     * @param clazz objeto do tipo class, representando a classe do novo objeto.
     * @return instância de T.
     */
    public static <T extends Object> T newInstance(Class<T> clazz) {
        T instance;
        try {
            instance = clazz.newInstance();
        } catch (InstantiationException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ERROR_INSTANTIATING.create(clazz.getName()), e);
        } catch (IllegalAccessException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ACCESS_TO_CONSTRUCTOR.create(clazz.getName()), e);
        }
        return instance;
    }

    /**
     * Retorna a classe do argumento <i>generic</i> da posição especificada da declaração da classe.
     * 
     * @param <T> Tipo da classe.
     * @param position posição do argumento.
     * @param clazz Classe
     * @param typeArgumentClazz Classe do argumento
     * @return Tipo da classe.
     */
    @SuppressWarnings("unchecked")
    public static <T extends Object> Class<T> getTypeArgumentClass(int position, Class<?> clazz, Class<T> typeArgumentClazz) {
        return (Class<T>) ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[position];
    }

    /**
     * @param clName String
     * @return boolean
     */
    public static boolean isClassPresent(String clName) {
        try {
            findClass(clName);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    /**
     * Devolve o método get do atributo especificado.
     * 
     * @param clazz Classe
     * @param attributeName nome do atributo
     * @return Método get do atributo especificado
     */
    public static Method getReadAccessor(Class<?> clazz, String attributeName) {
        String methodName = "get" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1);
        try {
            return clazz.getMethod(methodName, new Class[]{});
        } catch (SecurityException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_UNABLE_TO_ACCESS_METHOD.create(methodName, clazz.getName()), e);
        } catch (NoSuchMethodException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_NO_SUCH_METHOD.create(methodName, clazz.getName()), e);
        }
    }

    /**
     * Devolve o método get do atributo especificado.
     * 
     * @param clazz Classe
     * @param attributeName nome do atributo
     * @return Método set do atributo especificado
     */
    public static Method getWriteAccessor(Class<?> clazz, String attributeName) {
        String methodName = "set" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1);
        try {
            Class<?> type = ReflectionUtils.getReadAccessor(clazz, attributeName).getReturnType();
            return clazz.getMethod(methodName, new Class[]{ type });
        } catch (SecurityException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_UNABLE_TO_ACCESS_METHOD.create(methodName, clazz.getName()), e);
        } catch (NoSuchMethodException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_NO_SUCH_METHOD.create(methodName, clazz.getName()), e);
        }
    }

    /**
     * Altera o valor do atributo especificado.
     * 
     * @param object objeto
     * @param attributeName nome do atributo
     * @param value valor do atributo.
     */
    public static void setValue(Object object, String attributeName, Object value) {
        Method method = ReflectionUtils.getWriteAccessor(object.getClass(), attributeName);
        try {
            method.invoke(object, new Object[]{ value });
        } catch (IllegalArgumentException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ARGUMENT.create(value, attributeName, object.getClass()
                    .getName()), e);
        } catch (IllegalAccessException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ACCESS_TO_SET_METHOD.create(attributeName, object
                    .getClass().getName()), e);
        } catch (InvocationTargetException e) {
            rethrowErrorOrRuntime(e);
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ERROR_ACCESSING_SET_METHOD.create(attributeName, object.getClass()
                    .getName()), e);
        }
    }

    /**
     * Recupera o valor do atributo especificado.
     * 
     * @param object objeto
     * @param attributeName nome do atributo
     * @return valor do atributo
     */
    public static Object getValue(Object object, String attributeName) {
        Method getMethod = ReflectionUtils.getReadAccessor(object.getClass(), attributeName);
        try {
            return getMethod.invoke(object, new Object[]{});
        } catch (IllegalAccessException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ACCESS_TO_GET_METHOD.create(attributeName, object
                    .getClass().getName()), e);
        } catch (InvocationTargetException e) {
            rethrowErrorOrRuntime(e);
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ERROR_ACCESSING_GET_METHOD.create(attributeName, object.getClass()
                    .getName()), e);
        }
    }

    /**
     * Relanca a excecao alvo da {@link InvocationTargetException} se esta for uma {@link RuntimeException} ou uma {@link Error}
     * 
     * @param e {@link InvocationTargetException}
     */
    public static void rethrowErrorOrRuntime(InvocationTargetException e) {
        if (e.getTargetException() instanceof RuntimeException) {
            throw (RuntimeException) e.getTargetException();
        } else if (e.getTargetException() instanceof Error) {
            throw (Error) e.getTargetException();
        }
    }

    /**
     * Relanca a excecao alvo da {@link InvocationTargetException} se esta for a excecao esperada
     * 
     * @param <E> o tipo esperado da excecao
     * @param e {@link InvocationTargetException}
     * @param exceptionType o tipo da exception
     * @throws E lancada
     */
    @SuppressWarnings("unchecked")
    public static <E extends Exception> void throwSpecificException(InvocationTargetException e, Class<E> exceptionType) throws E {
        if (exceptionType.isInstance(e.getTargetException())) {
            throw (E) e.getTargetException();
        }
    }

    /**
     * Verifica se um objeto eh um java enum
     * 
     * @param obj Object
     * @return boolean
     */
    public static boolean isEnumInstance(Object obj) {
        return Enum.class.isInstance(obj);
    }

    /**
     * Se o objeto passado for um enum, devolve o tipo que define os enums - java enums sao criados por subclasses anonimas da classe que a
     * define
     * 
     * @param instance Object
     * @return Class ou null se o objeto passado nao for um enum
     */
    public static Class<?> getEnumType(Object instance) {
        if (!isEnumInstance(instance)) {
            return null;
        }
        Class<?> theClass = instance.getClass();
        while (!theClass.isEnum()) {
            theClass = theClass.getSuperclass();
        }
        return theClass;
    }

    /**
     * Invokes on the given source the given method with the given args. Will throw a {@link NullPointerException} on null arguments.
     * 
     * @param source the source
     * @param method the method
     * @param args the args
     * @return the result of the invocation
     */
    public static Object invoke(Object source, String method, Object... args) {
        try {
            Method m = source.getClass().getMethod(method, ClassMapper.mapClasses(args));
            m.setAccessible(true);
            return m.invoke(source, args);
        } catch (SecurityException e) {
            throw new ReflectionUtilsException(
                    UtilsMessages.REFLECTION_UNABLE_TO_ACCESS_METHOD.create(method, source.getClass().getName()), e);
        } catch (NoSuchMethodException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_NO_SUCH_METHOD.create(method, source.getClass().getName()), e);
        } catch (IllegalArgumentException e) {
            throw new ReflectionUtilsException(
                    UtilsMessages.REFLECTION_ILLEGAL_ARGUMENT_METHOD.create(method, source.getClass().getName()), e);
        } catch (IllegalAccessException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ACCESS_METHOD.create(method, source.getClass().getName()),
                    e);
        } catch (InvocationTargetException e) {
            rethrowErrorOrRuntime(e);
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ERROR_ACCESSING_METHOD.create(method, source.getClass().getName()),
                    e);
        }
    }

    /**
     * Finds and invokes a static method on the given class.
     * 
     * @param clazz the class
     * @param method the method
     * @param args the arguments
     * @return the result of the invocation
     */
    public static Object invokeStatic(Class<?> clazz, String method, Object... args) {
        try {
            Method m = clazz.getMethod(method, ClassMapper.mapClasses(args));
            m.setAccessible(true);
            return m.invoke(null, args);
        } catch (SecurityException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_UNABLE_TO_ACCESS_METHOD.create(method, clazz.getName()), e);
        } catch (NoSuchMethodException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_NO_SUCH_METHOD.create(method, clazz.getName()), e);
        } catch (IllegalArgumentException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ARGUMENT_METHOD.create(method, clazz.getName()), e);
        } catch (IllegalAccessException e) {
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ILLEGAL_ACCESS_METHOD.create(method, clazz.getName()), e);
        } catch (InvocationTargetException e) {
            rethrowErrorOrRuntime(e);
            throw new ReflectionUtilsException(UtilsMessages.REFLECTION_ERROR_ACCESSING_METHOD.create(method, clazz.getName()), e);
        }
    }

    /**
     * Mapper from {@link Object} to {@link Class}
     * 
     * @author jrenaut
     */
    public static final class ClassMapper implements Mapper<Object, Class> {

        public static final Mapper<Object, Class> INSTANCE = new ClassMapper();

        /**
         * @param objs a list of objects
         * @return a list of classes
         */
        public static Class[] mapClasses(Object[] objs) {
            return mapClasses(Arrays.asList(objs)).toArray(new Class[objs.length]);
        }

        /**
         * @param objs a list of objects
         * @return a list of the classes
         */
        public static List<Class> mapClasses(List<Object> objs) {
            return MapperUtils.mapList(objs, INSTANCE);
        }

        /**
         */
        private ClassMapper() {
        }

        /**
         * {@inheritDoc}
         */
        public Class apply(Object input) {
            return input.getClass();
        }

    }

}
